package de.axone.wash;

import java.io.UnsupportedEncodingException;
import java.util.Formatter;

import com.Ostermiller.util.Base64;

import de.axone.wash.Wash.CodingException;
import de.axone.wash.Wash.Type;
import de.axone.wash.Wash.WashException;



public class WashTools {
	
	private static String base64Encode( byte[] data ) {
		
		return new String( Base64.encode( data ) );
	}
	
	private static byte[] base64Decode( String data ) throws CodingException {
		
		try {
			return Base64.decode( data ).getBytes();
		} catch( Exception e ) {
			
			throw new CodingException( "Cannot decode base64" );
		}
	}
	
	private static String stringEncode( String data ){
		
		char[] ch = data.toCharArray();
		
		// First replace returns and backslashes
		StringBuilder result = new StringBuilder();
		for( int i = 0; i < ch.length; i++ ) {
			
			if( ch[ i ] == '\n' ) {
				
				result.append( "\\n" );
			
			} else if( ch[ i ] == '\\' ) {
				
				result.append( "\\\\" );
					
			} else {
				result.append( ch[ i ] );
			}
		}
		
		// Rebuild String
		String temp = result.toString();
		
		// Convert to utf-8 byte block
		byte[] buffer=null;
		try {
			buffer = temp.getBytes( "utf-8" );
		} catch( UnsupportedEncodingException e ) {}
		
		// Replace non printables with escape sequences
		result = new StringBuilder();
		Formatter f = new Formatter( result );
		for( int i = 0; i < buffer.length; i++ ) {
			
			byte b = buffer[ i ];
			
			if( b < 0x20 || b > 0x7e ) {
				
				f.format( "\\%02x", b );
			} else {
				result.append( (char) b );
			}
		}
		
		return result.toString();
	}
	
	private static String stringDecode( String data ) throws UnsupportedEncodingException {
		
		char[] c = data.toCharArray();
		byte[] buffer = new byte[ c.length ];
		StringBuilder result = new StringBuilder();
		
		// First copy Sting in byte array and decode \00 values
		
		int j = 0;
		for( int i = 0; i < c.length; i++ ) {
			
			char ch = c[ i ];
			
			if( ch == '\\' ) {
				
				if( i+2 < c.length && isHexChar( c[ i+1 ] )  && isHexChar( c[ i+2 ] ) ) {
					
					try {
						buffer[ j++ ] = (byte)Short.valueOf( "" + c[ i+1 ] + c[ i+2 ], 16 ).shortValue();
						
					} catch( NumberFormatException e ) {
						throw new UnsupportedEncodingException();
					}
					
					i+=2;
					
					continue;
					
				} else if( i+1 < c.length && c[i+1] == '\\' || c[i+1] == 'n' ){
					buffer[ j++ ] = (byte)c[i++];
					buffer[ j++ ] = (byte)c[i];
					continue;
				} else {
					throw new UnsupportedEncodingException( "Not a valid escape sequence at pos " + i );
				}
			}
			
			if( c[ i ] < 0x20 || c[ i ] > 0x7e )
				throw new UnsupportedEncodingException();
				
			buffer[ j++ ] = (byte)c[ i ];
		}
		
		String temp = new String( buffer, 0, j, "utf-8" );
		
		
		for( int i = 0; i < temp.length(); i++ ) {
			
			char ch = temp.charAt( i );
			
			// escape
			if( ch == '\\' ) {
				
				if( i == temp.length() )
					throw new UnsupportedEncodingException();
				
				i++;
				ch = temp.charAt( i );
				
				if( ch == 'n' ) {
					result.append( '\n' );
					
				} else if( ch == '\\' ) {
					result.append( '\\' );
					
				} else {
					
					throw new UnsupportedEncodingException();
				}
				
			} else {
				result.append( ch );
			}
		}
		
		return result.toString();
	}
	
	private static boolean isHexChar( char c ) {
		
		return ( c >= '0' && c <= '9' || c >= 'a' && c <= 'f' || c >= 'A' && c <= 'F' );
	}
	
	public static TypeValue decodeValue( String line ) throws WashException {
		
		char indicator = line.charAt( 0 );
		String data = line.substring( 1 );
		
		if( "NULL".equals( line ))
			return new TypeValue( null, null );
		
		switch( indicator ) {
		
		case '"':
			
			String s = decodeString( data );
			
			return new TypeValue( Type.STRING, s );
			
		case 'i':
			
			Long i = decodeLong( data );
			
			return new TypeValue( Type.INTEGER, i );
			
		case 'f':
			
			Double f = decodeDouble( data );
			
			return new TypeValue( Type.FLOAT, f );
			
		case 'b':
			
			Boolean b = decodeBoolean( data );
			
			return new TypeValue( Type.BOOLEAN, b );
			
		case 'd':
			
			byte[] d = decodeData( data );
			
			return new TypeValue( Type.DATA, d );
			
		default:
			throw new CodingException( "Unknown type: " + indicator );
		}
	}
	
	public static String decodeString( String s ) throws CodingException {
		
		try {
			String value = WashTools.stringDecode( s );
				
			return value;
				
		} catch( UnsupportedEncodingException e ) {
			throw new CodingException( "Cannot decode string", e );
		}
	}
	
	public static Long decodeLong( String s ) throws CodingException {
		
		try {
			Long value = Long.valueOf( s, 10 );
			
			return value;
			
		} catch( NumberFormatException e ) {
			throw new CodingException( "Cannot decode integer", e );
		}
	}
	
	public static Double decodeDouble( String s ) throws CodingException {
		
		try {
			Double value = Double.valueOf( s );
			
			return value;
			
		} catch( NumberFormatException e ) {
			throw new CodingException( "Cannot decode float", e );
		}
	}
	
	public static Boolean decodeBoolean( String s ) throws CodingException {
		
		if( s.equals( "TRUE" ) ) {
			
			return Boolean.TRUE;
			
		} else if( s.equals( "FALSE" ) ) {
			
			return Boolean.FALSE;
		} else {
			throw new CodingException( "Cannot decode boolean" );
		}
	}
	
	public static byte[] decodeData( String s ) throws CodingException {
		
		return WashTools.base64Decode( s );
	}
	
	public static String encodeString( String s ){
		
		return stringEncode( s );
	}
	public static String encodeLong( Long i ){
		
		return i.toString();
	}
	public static String encodeDouble( Double f ){
		
		return f.toString();
	}
	public static String encodeBoolean( Boolean b ){
		
		return ( b == Boolean.TRUE ? "TRUE" : "FALSE" );
	}
	public static String encodeData( byte[] d ){
		
		return base64Encode( d );
	}
	
	
	public static class TypeValue {
		
		public Type type;
		public Object value;
		
		public TypeValue( Type type, Object value ) {
			
			this.type = type;
			this.value = value;
		}
	}
}
